import os
import pywintypes
import win32com.client as com
import numpy as np
import random
import csv
import Corflo_util as util
import Corflo_RM as RM


def runSimulation(params):

    link_groups = [
                {'MAINLINE': (1,),            'ONRAMP': (),             'OFFRAMP': (),          'DC': 1, 'VSL': (6,7,8,9,10)},
                {'MAINLINE': (2,),            'ONRAMP': (3,),           'OFFRAMP': (4,),        'DC': 2, 'VSL': (11,12,13,14,15,16)},
                {'MAINLINE': (5,),            'ONRAMP': (6,),           'OFFRAMP': (7,),        'DC': 3, 'VSL': (17,18,19,20,21,22)},
                {'MAINLINE': (8,20),          'ONRAMP': (),             'OFFRAMP': (9,),        'DC': 4, 'VSL': (23,24,25,26,27)},
                {'MAINLINE': (10,19),         'ONRAMP': (11,),          'OFFRAMP': (),          'DC': 5, 'VSL': (28,29,30,31,32,33)},
                {'MAINLINE': (12,),           'ONRAMP': (14,),          'OFFRAMP': (13,),       'DC': 6, 'VSL': (34,35,36,37,38,39)},
                {'MAINLINE': (15,10016),      'ONRAMP': (16,),          'OFFRAMP': (17,),       'DC': 7, 'VSL': (40,41,42,43,44,45)},
                {'MAINLINE': (18,),           'ONRAMP': (),             'OFFRAMP': (),          'DC': 8, 'VSL': (46,47,48,49,50)},
    ]
    
    ramps = {
                3: {'LINK_GROUP': 2, 'TYPE': 'ON', 'IDX': 0, 'DC': 9, 'SC': 1, 'QC': 1, 'Qcap': 460},
                4: {'LINK_GROUP': 2, 'TYPE': 'OFF', 'DC': 10},
                6: {'LINK_GROUP': 3, 'TYPE': 'ON', 'IDX': 1, 'DC': 11, 'SC': 2, 'QC': 2, 'Qcap': 460},
                7: {'LINK_GROUP': 3, 'TYPE': 'OFF', 'DC': 12},
                9: {'LINK_GROUP': 4, 'TYPE': 'OFF', 'DC': 13},
                11: {'LINK_GROUP': 5, 'TYPE': 'ON', 'IDX': 2, 'DC': 14, 'SC': 3, 'QC': 3, 'Qcap': 420},
                14: {'LINK_GROUP': 6, 'TYPE': 'ON', 'IDX': 3, 'DC': 16, 'SC': 4, 'QC': 4, 'Qcap': 300},
                13: {'LINK_GROUP': 6, 'TYPE': 'OFF', 'DC': 15},
                16: {'LINK_GROUP': 7, 'TYPE': 'ON', 'IDX': 4, 'DC': 17, 'SC': 5, 'QC': 5, 'Qcap': 280},
                17: {'LINK_GROUP': 7, 'TYPE': 'OFF', 'DC': 18},
    }
    
    # 0: no incident
    # 1: an incident at on-ramp merging area
    scenarios = [{'group': 0, 'link': 0, 'lane': (), 'coordinate': 0, 'startTime_sec': 99960, 'endTime_sec': 99990},
                 {'group': 5, 'link': 19, 'lane': (1,), 'coordinate': 60, 'startTime_sec': 600, 'endTime_sec': 1800}]
    
    Nsec = len(link_groups)     # number of sections
    simResolution = 5           # No. of simulation steps per second
    stepTime_sec = 1 / simResolution
    Tdata_sec = 30.0            # Data sampling period
    Tctrl_sec = 30.0            # Control time interval
    randseed = round(random.uniform(1,1000))
    scenario = scenarios[params["scenario"]]      
    startTime_sec = scenario["startTime_sec"]
    endTime_sec = scenario["endTime_sec"]
    demands = params["demands"]
    vf = 100                    # The free flow speed of highway in km/h
    C = 12000                   # freeway capacity
    Nlaneclosed = len(scenario["lane"])
    Nlane = 5
    Cd = C * (Nlane - Nlaneclosed) / Nlane
    rho_star = min(demands[0], Cd-400) / vf
    
    # initialze measurements and control variables
    speed = [0.0] * Nsec
    density = [0.0] * Nsec
    flow = [0.0] * Nsec
    err_accum = [0.0] * Nsec
    vsl = [vf] * Nsec    
    
    rampFlow = {}
    rampQue = {}
    rmRate = {}
    Nonramp = 0
    for key in ramps:
        rampFlow[key] = 0.0
        rampQue[key] = 0.0
        rmRate[key] = 0.0
        if ramps[key]["TYPE"] == "ON": Nonramp += 1

    # initialze a csv file to record flow, density and queue length
    ctrlnum = 1000*params["ctrlmodes"][0] + 100*params["ctrlmodes"][1] + 10*params["ctrlmodes"][2] + params["ctrlmodes"][3]
    FDQfileName = os.path.join(params["respath"], "D{0}A{1}_S{2}C{3}.csv".format(demands[0], sum(demands[1:]), params["scenario"], ctrlnum))
    FDQfile = open(FDQfileName, 'a', newline='')
    FDQwriter = csv.writer(FDQfile)
    
            
    # start VISSIM simulation 
    ProgID = "VISSIM.Vissim.1000"
    networkFileName = "Corflo_Roadnet.inpx"
    layoutFileName = "Corflo_Roadnet.layx"
    try:
        print("Client: Creating a Vissim instance")
        vs = com.Dispatch(ProgID)
    
        print("Client: read network and layout")
        vs.LoadNet(os.path.join(params["filepath"], networkFileName), 0)
        vs.LoadLayout(os.path.join(params["filepath"], layoutFileName))
    
        print("Client: Setting simulations")
        vs.Simulation.SetAttValue('SimRes', simResolution)
        vs.Simulation.SetAttValue('UseMaxSimSpeed', True)        
        vs.Simulation.SetAttValue('RandSeed', randseed)
        #vs.Simulation.SetAttValue('SimSpeed', 2.0)
        #vs.Presentation.SetAttValue('RecordAnimations', True)
        
        net = vs.Net        # Reference to road network
        dataCollections = net.DataCollectionMeasurements
        links = net.Links
        vehs = net.Vehicles
        signalControllers = net.SignalControllers
        queueCounters = net.QueueCounters
        queue = [0.0] * Nonramp
        Nintersec = (queueCounters.count - Nonramp) // 4
        queueIntersec = [0.0] * Nintersec
        
        # write header
        header = []
        for i in range(Nsec):
            header.append("flow{}".format(i))
        for i in range(Nsec):
            header.append("den{}".format(i))
        for i in range(Nonramp):
            header.append("Qramp{}".format(i))
        for i in range(Nintersec):
            header.append("Qintersec{}".format(i))
        FDQwriter.writerow(header)
        
        # set vehicle inputs
        vehInputs = net.VehicleInputs
        for i in range(vehInputs.Count):
            vehInput = vehInputs.ItemByKey(1+i)
            vehInput.SetAttValue('Volume(1)', demands[i])
        
            
        # set default speed limit and 1st sign location
        VSLs = net.DesSpeedDecisions        
        for VSL in VSLs:
            VSL.SetAttValue("DesSpeedDistr(10)", vf)   # car (km/h)
            VSL.SetAttValue("DesSpeedDistr(20)", vf)   # HGV (km/h)
        
        pos = 4001 - params["L0"]*1000
        for i in link_groups[0]["VSL"]:
            VSLs.ItemByKey(i).SetAttValue("Pos", pos)
        
        # set active duration for VSL signs after the bottleneck, hard code
        if params["scenario"] == 1:
            for vslID in range(51, 56):
                VSL = VSLs.ItemByKey(vslID)
                VSL.SetAttValue("TimeFrom", startTime_sec)
                VSL.SetAttValue("TimeTo", endTime_sec)
        else:
            for vslID in range(51, 56):
                VSL = VSLs.ItemByKey(vslID)
                VSL.SetAttValue("TimeFrom", 30)
                VSL.SetAttValue("TimeTo", 60)
            
        # set vehicle routings
        params_Routes = {
            "appID": range(1,29),
            "onrampID": range(29,34),
            "appRF": [0.2,0.6,0.2],
            "onrampRF": [0.7,0.3],
            "changedID": [9,10,14,31],
            "changedRF": [[0.2,0.4,0.4],[0.15,0.65,0.2],[0.4,0.4,0.2],[0.7,0.3]],
            "changedPos": ["AppE","AppS","AppS","Ramp"],
            "changedIdx": [3,3,4,2],
            "std-mean-ratio": params["perturbations"][4]
        }
        util.setVehRouting(net, params=params_Routes, change_flag=False)        
        
        # estimate the demands for intersections and on-ramps
        d_intersecs, d_onramps = util.getEstDemands(demands, params_Routes, change_flag=False)
        dc_intersecs, dc_onramps = util.getEstDemands(demands, params_Routes, change_flag=True)
        est_onramp_demands = d_onramps
        
        # compute optimal cycle based on the demands
        OptCycs = util.getOptCycle(d_intersecs)
        OptCycs_inc = util.getOptCycle(dc_intersecs)
        
        # set signal programs
        params_SC = {
            "rampSC": [1,2,3,4,5],
            "intSC": [6,7,8,9,10,11,12],
            "intSCoffset": [0,0,0,0,0,0,0,0],
            "intSC_crit": [9,10],
            "prog_crit_optcyc": [1,3,5],
            "prog_crit_defcyc": [2,4,6],
            "default_cyc": 120,
            "TSCmode": params["ctrlmodes"][3]
        }
        util.Arterial_TSC(signalControllers, params_SC, OptCycs, inci_stage=0)

        # Make sure that all lanes are open      
        link_Nlane = {} 
        link_length = {}
        for link in links:
            ID = link.AttValue('No')
            link_Nlane[ID] = link.AttValue('NumLanes')
            link_length[ID] = link.AttValue('Length2D')
            for lane in link.Lanes:
                lane.SetAttValue('BlockedVehClasses', '')
                 
        # compute the length of each section
        section_length = [0.0] * Nsec
        for iSec in range(len(link_groups)): 
            for linkID in link_groups[iSec]['MAINLINE']:
                link = links.ItemByKey(linkID)
                section_length[iSec] += link.AttValue('Length2D') #meter

        # initialize VSL parameters
        incidentSection = len(link_groups) - 1
        for group in link_groups:
            if scenario["link"] in group["MAINLINE"]:
                incidentSection = link_groups.index(group)
                break
            
        params_VSL = {
            "mode": params["ctrlmodes"][0],
            "C": C,
            "Cd": Cd,
            "vf": vf,
            "rho_star": rho_star,
            "scenario": scenario,
            "link_groups": link_groups,
            "perturbations": params["perturbations"],
            "sec_len": section_length,
            "min_values": [30, 70],
            "start_end": [0, incidentSection],
            "w": 30
        }
        
        # use static buses to block the lanes and create the incident
        busNo = 2 * len(scenario["lane"]) 
        busArray = [0] * busNo
        
        # Construct the dictionary of ramp meters
        meters_obj = {}
        for key in ramps.keys():
            ramp = ramps[key]
            if ramp['TYPE'] == 'ON':
                meters_obj[key] = RM.RampMeter(ramp['LINK_GROUP'], ramp['IDX'], dataCollections.ItemByKey(ramp['DC']), signalControllers.ItemByKey(ramp['SC']), queueCounters.ItemByKey(ramp['QC']), ramp['Qcap'], rho_star)
                if params["ctrlmodes"][2]:
                    rmRate[key] = max(est_onramp_demands[ramp['IDX']], 400)
                else:
                    rmRate[key] = 1800
                meters_obj[key].update_rate(rmRate[key])
                
        
        # start simulation
        print("Client: Starting simulation")
        print("Scenario: {0}   Control: {1}   Random Seed: {2}".format(params["scenario"], ctrlnum, randseed))

        currentTime = 0
        while currentTime <= params["simtime"]:
            # run sigle step of simulation
            vs.Simulation.RunSingleStep()
            currentTime = vs.Simulation.AttValue('SimSec')
            for key in meters_obj:
                meters_obj[key].meter_step(stepTime_sec)

            if 0 == currentTime % Tdata_sec:
                # Get density, flow, speed of each section
                for iSection in range(Nsec):
                    dataCollection = dataCollections.ItemByKey(link_groups[iSection]['DC'])                    
                    flow[iSection] = (3600/Tdata_sec)*dataCollection.AttValue('Vehs(Current,Last,All)')
                    Nveh = 0
                    dist = 0
                    denomenator = 0
                    for linkID in link_groups[iSection]['MAINLINE']:
                        link = links.ItemByKey(linkID)
                        link_vehs = link.Vehs
                        num_vehs = link_vehs.Count
                        sum_speed = 0
                        if num_vehs == 0:
                            v = 0
                        else:
                            for link_veh in link_vehs:
                                sum_speed += link_veh.AttValue('Speed')
                            v = sum_speed / num_vehs
                        Nveh += link_vehs.Count
                        dist += v * link_vehs.Count #total distance traveled by all vehicles in one link
                        denomenator += link_length[linkID]
                    density[iSection] = 1000 * Nveh / denomenator #number of vehicles per km
                    if 0 == Nveh:
                        speed[iSection] = 0
                    else:
                        speed[iSection] = dist / Nveh #km/h

                for i in range(len(queue)):
                    queue[i] = queueCounters.ItemByKey(i+1).AttValue('QLen(Current,Last)')
                # compute average queue length of 4 directions for each intersection
                for i in range(Nintersec):
                    st_idx = 4 * i + Nonramp + 1
                    q1 = queueCounters.ItemByKey(st_idx).AttValue('QLen(Current,Last)')
                    q2 = queueCounters.ItemByKey(st_idx+1).AttValue('QLen(Current,Last)')
                    q3 = queueCounters.ItemByKey(st_idx+2).AttValue('QLen(Current,Last)')
                    q4 = queueCounters.ItemByKey(st_idx+3).AttValue('QLen(Current,Last)')
                    queueIntersec[i] = (q1 + q2 + q3 + q4) / 4
                
                # write csv file
                row = flow + density + queue + queueIntersec
                FDQwriter.writerow(row)
                
                for key in ramps.keys():
                    dataCollection = dataCollections.ItemByKey(ramps[key]['DC'])
                    rampFlow[key] = (3600/Tdata_sec)*dataCollection.AttValue('Vehs(Current,Last,All)')
                    
            if currentTime == startTime_sec:
                # set incident scenario
                i = 0
                for lane in scenario['lane']:
                    busArray[i] = vehs.AddVehicleAtLinkPosition(300, scenario['link'], lane, scenario['coordinate'], 0, 0)
                    busArray[i+1] = vehs.AddVehicleAtLinkPosition(300, scenario['link'], lane, scenario['coordinate']+20, 0, 0)
                    i += 2
                
                # Apply Lane change control
                if params["ctrlmodes"][1]:
                    util.LC_Control(scenario, links, link_groups, params["dLC"], params["ctrlmodes"][1])
                
                # Apply arterial traffic signal control and change vehicle routings
                if params["ctrlmodes"][3]:
                    util.Arterial_TSC(signalControllers, params_SC, OptCycs_inc, inci_stage=1)
                
                util.setVehRouting(net, params=params_Routes, change_flag=True)
                
                # Change estimated onramp demands
                est_onramp_demands = dc_onramps

            if currentTime == endTime_sec:
                # Remove the incident
                for veh in busArray:
                    vehs.RemoveVehicle(veh.AttValue('No'))

                # open all closed lanes
                for link in links:
                    for lane in link.Lanes:
                        lane.SetAttValue('BlockedVehClasses', '')
                
                # Restore arterial signal program and vehicle routings
                util.Arterial_TSC(signalControllers, params_SC, OptCycs, inci_stage=2)
                util.setVehRouting(net, params=params_Routes, change_flag=False)
                
                # Change estimated onramp demands back
                est_onramp_demands = d_onramps
            
            
            #Compute the VSL command and the RM rate
            if 0 == currentTime % Tctrl_sec:
                
                if params["ctrlmodes"][2]:
                    for key in meters_obj.keys():
                        rampQue[key] = meters_obj[key].QC.AttValue('QLen(Current,Last)')
                        ramp = meters_obj[key]
                        rmRate[key] = util.AlineaQ(rmRate[key], density[ramp.LINK_GROUP], rampQue[key], est_onramp_demands[ramp.IDX], meters_obj[key].Qcap, meters_obj[key].RHO)
                        ramp.update_rate(rmRate[key])
                
                if startTime_sec <= currentTime < endTime_sec:
                    if 0 < params["ctrlmodes"][0] < 9:
                        vsl, err_accum = util.VSL_FBL(params_VSL, density, flow, vsl, err_accum, rampFlow)
                    else: 
                        vsl = util.VSL_NoCtrl(params_VSL, vsl)
                else:
                    vsl = vf*np.ones(np.shape(vsl))

                #Update VSL command in VISSIM
                for iSec in range(Nsec): 
                    for vslID in link_groups[iSec]["VSL"]:
                        VSL = VSLs.ItemByKey(vslID)
                        VSL.SetAttValue("DesSpeedDistr(10)", vsl[iSec])   # car
                        VSL.SetAttValue("DesSpeedDistr(20)", vsl[iSec])   # HGV

        FDQfile.close()


    except pywintypes.com_error as err:
        print("err=", err)

        '''
        Error
        The specified configuration is not defined within VISSIM.
        
        Description
        Some methods for evaulations results need a previously configuration for data collection. 
        The error occurs when requesting results that have not been previously configured.
        For example, using the GetSegmentResult() method of the ILink interface to request
        density results can end up with this error if the density has not been requested within the configuration
        ''' 
        

def setParameters():
    params = {
        "scenario": 1,
        # [VSL, LC, RM, TSC];  0: inactive;  1: basic version;  2+: variations
        "ctrlmodes": [1,1,1,1],
        # vehicle inputs: [freeway entrance, arterial 1, arterial 2, ...]
        "demands": [12000, 1200, 800, 100, 300, 400, 200, 800, 800, 800, 800, 800, 800, 800, 800, 800, 800],
        # uncertainties in measurements and model parameters: [mainstream flow, mainstream density, w, queue length, std-mean ratio in vehicle routings]
        "perturbations": [0.0, 0.0, 0.0, 0.0, 0.1],
        # distance between first two VSL signs / length of section 0, must be in [0 , 4] (km)
        "L0": 2.0,
        # desired distance to apply LC recommendation (m)
        "dLC": 800,
        # simulation time (sec)
        "simtime": 2400,
        # number of random simulations to be run
        "simnum": 5,
        # path of VISSIM files
        "filepath": os.path.abspath(os.curdir),
        # path of result files
        "respath": os.path.join(os.path.abspath(os.curdir), 'MicroSimResults/IncidentAtRampEntrance')
    }
    
    return params

                    

if __name__ == '__main__':
    params = setParameters()
    util.mkdir(params["respath"])
    for i in range(params["simnum"]):
        runSimulation(params)







    
